﻿#region Using

using System;
using System.IO;
using System.Net;
using System.Reflection;
using System.Security.Authentication;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Vk.Utils;

#endregion

namespace Vk.Authorization
{
    /// <summary>
    ///     Скрытая авторизация
    /// </summary>
    public class HiddenAuth : AuthClientBase
    {
        private CookieContainer _cookies;

        public VkSession Session { get; private set; }

        /// <summary>
        ///     Контейнер с cookies
        /// </summary>
        private CookieContainer Cookies
        {
            get { return _cookies ?? (_cookies = new CookieContainer()); }
            set { _cookies = value; }
        }

        public HiddenAuth(int appId, ApiAccessPermissions permissions = null) : base(appId, permissions)
        {
        }

        /// <summary>
        ///     Авторизует пользователя в сети Вконтакте
        /// </summary>
        /// <param name="login">Моб. телефон или email</param>
        /// <param name="pass">Пароль</param>
        public async Task<bool> AuthAsync(string login, string pass)
        {
            var session = new VkSession(AppId);

            //Выключение заголовка Expect
            ServicePointManager.Expect100Continue = false;

            try
            {
                //Получение HTML документа со страницей авторизации
                var doc = await GetHtmlCodeAsync(AuthPath);

                //Забираем со страницы нужные значения
                var format = "<input type=\"hidden\" name=\"{0}\" value=\"(?'res'[0-9A-Za-z:\\/._&?-]+)\"[ \\/]*>";
                var ipH = Regex.Match(doc, string.Format(format, "ip_h")).Groups["res"].Value;
                var lgH = Regex.Match(doc, string.Format(format, "lg_h")).Groups["res"].Value;
                var to = Regex.Match(doc, string.Format(format, "to")).Groups["res"].Value;
                var origin = Regex.Match(doc, string.Format(format, "_origin")).Groups["res"].Value;

                //Отправляем POST со всеми данными на сервер авторизации
                var response = await
                               PostAsync(
                                   "ip_h=" + ipH + "&lg_h=" + lgH + "&_origin=" + origin + "&to=" + to + "&expire=0" +
                                   "&email=" +
                                   login + "&pass=" +
                                   pass, "https://login.vk.com/?act=login&soft=1", AuthPath);

                if (!TryGetToken(response.ResponseUri, ref session)) //Проверяем успешность авторизации
                {
                    //Вконтакте просит подтверджение авторизации
                    string source;
                    using (var stream = response.GetResponseStream())
                    {
                        using (var sr = new StreamReader(stream))
                        {
                            source = await sr.ReadToEndAsync(); //Получаем html страницы
                        }
                    }
                    var allowUrl =
                        Regex.Match(source,
                                    "<form method=\"post\" action=\"(?'res'[[0-9a-zA-Z.:\\/?=&_%]+)\"[ \\/]*>").Groups[
                                        "res"]
                            .Value;

                    //Делаем GET-запрос на полученный url
                    response = await CommonUtils.HttpRequest(allowUrl, Cookies);

                    //Проверяем успешность авторизации еще раз
                    if (!TryGetToken(response.ResponseUri, ref session))
                        throw new AuthenticationException(
                            "Авторизация не закончилась успехом за нужное количество попыток");
                }

                Session = session;

                //Вызываем событие успешной авторизации
                FireEvent(AuthSuccessEvent, new AuthSuccessEventArgs(session));
                return true;
            }
            catch (AuthenticationException exc)
            {
                FireEvent(AuthFailEvent, new AuthFailEventArgs(AuthFailEventArgs.FailType.APIError, exc.Message));
                return false;
            }
            catch (Exception exc)
            {
                FireEvent(AuthFailEvent, new AuthFailEventArgs(AuthFailEventArgs.FailType.LocalException, exc.Message));
                return false;
            }
        }

        /// <summary>
        ///     Возвращает HTML код указанной страницы
        /// </summary>
        /// <param name="url">URL страницы</param>
        private async Task<string> GetHtmlCodeAsync(string url)
        {
            var response = await CommonUtils.HttpRequest(url, Cookies);
            using (var stream = response.GetResponseStream())
            {
                using (var sr = new StreamReader(stream))
                {
                    return await sr.ReadToEndAsync(); //Получаем html страницы
                }
            }
        }

        /// <summary>
        ///     Отравляет POST-запрос на сервер авторизации
        /// </summary>
        /// <param name="data">Строка, которая будет внедрена в запрос</param>
        /// <param name="url">Адрес сервера</param>
        /// <param name="referer">Значение для заголовка Referer</param>
        /// <returns>Ответ на запрос</returns>
        private async Task<HttpWebResponse> PostAsync(string data, string url, string referer)
        {
            //Создаем запрос
            var req = (HttpWebRequest) WebRequest.Create(url);

            //Отключаем автоперенаправление
            req.AllowAutoRedirect = false;

            //Инициализируем запрос
            req.Headers.Add("Origin", "https://oauth.vk.com");
            req.Headers.GetType()
               .InvokeMember("ChangeInternal",
                             BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.InvokeMethod, null,
                             req.Headers, new object[] {"Connection", "keep-alive"});
            req.Headers.GetType()
               .InvokeMember("ChangeInternal",
                             BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.InvokeMethod, null,
                             req.Headers, new object[] {"Cache-Control", "max-age=0"});
            req.Headers.Add("Accept-Encoding", "gzip,deflate,sdch");
            req.Headers.Add("Accept-Language", "ru-RU,ru;q=0.8,en-US;q=0.6,en;q=0.4");
            req.UserAgent =
                "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.6 Safari/537.36";
            req.Referer = referer;
            req.CookieContainer = Cookies;
            req.Accept = "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8";
            req.Method = WebRequestMethods.Http.Post;
            req.Timeout = 30000; //30 c
            req.ContentType = "application/x-www-form-urlencoded";

            //Записываем data в RequestStream
            var sentData = Encoding.UTF8.GetBytes(data);
            req.ContentLength = sentData.Length;
            using (var inputStream = req.GetRequestStream())
            {
                inputStream.Write(sentData, 0, data.Length);
            }

            //Получаем ответ
            var res = (HttpWebResponse) await req.GetResponseAsync();

            //Сохраняем cookie
            Cookies.Add(res.Cookies);

            //Возвращаем ответ
            return res.Headers["Location"] != null
                       ? await CommonUtils.HttpRequest(res.Headers["Location"], Cookies)
                       : res;
        }

        /// <summary>
        ///     Пытается получить токен из URL
        /// </summary>
        /// <param name="respUri">URL</param>
        /// <param name="session">Обьект сессии, который нужно заполнить</param>
        /// <returns>true - если удалось</returns>
        private bool TryGetToken(Uri respUri, ref VkSession session)
        {
            //Поиск ошибки
            var isError = new Regex("error").IsMatch(respUri.Fragment);
            if (isError)
            {
                var parts = respUri.Fragment.Split('&');
                var type = parts[0].Replace("error=", "");
                var desc = parts[1].Replace("error_description=", "");
                throw new AuthenticationException(string.Format("{}: {}", type, desc));
            }

            //Инициализация регулярных выражений
            var accessTokenRegex = new Regex("access_token");
            var userIdRegex = new Regex("user_id");
            var expiresInRegex = new Regex("expires_in");

            //Разбиение url на куски
            var chunks = respUri.Fragment.Split(new[] {"&"}, StringSplitOptions.None);

            var authDone = false; //Флаг успешной авторизации
            //Проход по всем кускам и поиск нужной информации
            foreach (var chunk in chunks)
            {
                if (accessTokenRegex.IsMatch(chunk))
                {
                    session.AccessToken = chunk.Replace("access_token=", "")
                                               .Replace("#", "")
                                               .Replace("&", "");
                    authDone = true;
                }
                if (userIdRegex.IsMatch(chunk))
                    session.UserId =
                        int.Parse(chunk.Replace("user_id=", "").Replace("#", "").Replace("&", ""));
                if (expiresInRegex.IsMatch(chunk))
                    session.Expires =
                        DateTime.Now + TimeSpan.FromSeconds(double.Parse(chunk.Replace("expires_in=", "")
                                                                              .Replace("#", "")
                                                                              .Replace("&", "")));
            }
            return authDone;
        }
    }
}